package gable

import (
	"encoding/binary"
	"errors"
	"log"
	"os"
	"unsafe"

	"extractor/gable-reader/gable/stream"

	"golang.org/x/text/encoding/unicode"
)

const (
	MagicIdentifier  = -2226271756974174256
	HeaderSize       = 512
	DirEntrySize     = 128
	FirstIDListCount = 109
)

var UnicodeDecoder = unicode.UTF16(unicode.LittleEndian, unicode.IgnoreBOM).NewDecoder()

type CompoundDocumentHeader struct {
	Magic                     int64
	Uid                       [16]byte
	Reversion                 int16
	Version                   int16
	ByteOrder                 int16
	SectorSize                int16
	ShortSectorSize           int16
	_                         [6]byte
	ShortSectorActualSize     uint32
	SectorTotalForSAT         uint32
	FirstSectorIDForDirectory int32
	SectorActualSize          uint32
	StandardStreamMinSize     uint32
	FirstSectorIDForShortSAT  int32
	SectorTotalForShortSAT    uint32
	FirstSectorIDForMasterSAT int32
	SectorTotalForMasterSAT   uint32
	FirstIDListForMasterSAT   [FirstIDListCount]int32
}

type DirectoryEntry struct {
	Name             [64]byte
	NameBytesCount   int16
	Type             byte
	Color            byte
	LeftDirID        int32
	RightDirID       int32
	RoorDirID        int32
	Identifier       [16]byte
	UserFlags        int32
	CreateModifyTime [16]byte
	FirstSectorID    int32
	TotalStreamSize  uint32
	_                int32
}

type CompoundDocument struct {
	Header         *CompoundDocumentHeader
	buf            []byte
	IDsChains      []int32
	ShortIDsChains []int32
	SectorsBuffer  []byte
	Directory      map[string]*DirectoryEntry
}

func ReadTillNegative(buf []byte, data []int32) int32 {
	l := len(buf)
	i := int32(0)
	var d int32
	for l > 0 {
		d = (int32)(binary.LittleEndian.Uint32(buf[i*4 : i*4+4]))
		if d < 0 {
			break
		}
		data[i] = d
		i++
		l -= 4
	}
	return i
}

func ReadTillEnd(buf []byte, data []int32) int32 {
	l := len(buf)
	i := int32(0)
	for l > 0 {
		data[i] = (int32)(binary.LittleEndian.Uint32(buf[i*4 : i*4+4]))
		i++
		l -= 4
	}
	return i
}

func (doc *CompoundDocument) getMasterSector(idx uint32) ([]byte, int32) {
	offset := idx * doc.Header.SectorActualSize
	return doc.SectorsBuffer[offset : offset+doc.Header.SectorActualSize-4],
		(int32)(binary.LittleEndian.Uint32(doc.SectorsBuffer[doc.Header.SectorActualSize-4:]))
}

func (doc *CompoundDocument) getSector(idx uint32) []byte {
	offset := idx * doc.Header.SectorActualSize
	return doc.SectorsBuffer[offset : offset+doc.Header.SectorActualSize]
}

func (doc *CompoundDocument) getChain(start uint32) []uint32 {
	chain := make([]uint32, 1, 16)
	chain[0] = start
	next := doc.IDsChains[start]
	for next >= 0 {
		chain = append(chain, uint32(next))
		next = doc.IDsChains[next]
	}
	return chain
}

func (doc *CompoundDocument) getShortChain(start uint32) []uint32 {
	chain := make([]uint32, 1, 16)
	chain[0] = start
	next := doc.ShortIDsChains[start]
	for next >= 0 {
		chain = append(chain, uint32(next))
		next = doc.ShortIDsChains[next]
	}
	return chain
}

func (doc *CompoundDocument) GetStream(name string) (stream.Stream, error) {
	dir := doc.Directory[name]
	if dir == nil {
		return nil, errors.New("CompoundDocument.getStream: stream not found")
	}
	size := dir.TotalStreamSize
	if size < doc.Header.StandardStreamMinSize {
		return stream.New(doc.SectorsBuffer, doc.getShortChain(uint32(dir.FirstSectorID)), doc.Header.ShortSectorActualSize, size), nil
	} else {
		return stream.New(doc.SectorsBuffer, doc.getChain(uint32(dir.FirstSectorID)), doc.Header.SectorActualSize, size), nil
	}

}

func (doc *CompoundDocument) initHeaderAndSectionBuffer() error {
	header := *(**CompoundDocumentHeader)(unsafe.Pointer(&doc.buf))
	doc.Header = header
	if header.Magic != MagicIdentifier {
		return errors.New("不是Office 2003文档的格式")
	}
	header.SectorActualSize = 1 << header.SectorSize
	header.ShortSectorActualSize = 1 << header.ShortSectorSize
	doc.SectorsBuffer = doc.buf[HeaderSize:]
	return nil
}

func (doc *CompoundDocument) initIDsChains() error {
	header := doc.Header
	masterIDs := make([]int32, header.SectorTotalForSAT)

	i := int32(0)
	var tmp int32
	for ; i < FirstIDListCount; i++ {
		tmp = header.FirstIDListForMasterSAT[i]
		if tmp < 0 {
			break
		}
		masterIDs[i] = tmp
	}

	if header.FirstSectorIDForMasterSAT >= 0 {
		nextSectorID := header.FirstSectorIDForMasterSAT
		var masterSector []byte
		for nextSectorID >= 0 {
			masterSector, nextSectorID = doc.getMasterSector(uint32(nextSectorID))
			i = ReadTillNegative(masterSector, masterIDs[i:])
		}
	}

	IDs := make([]int32, header.SectorTotalForSAT*header.SectorActualSize>>2)
	i = 0
	for _, mid := range masterIDs {
		sector := doc.getSector(uint32(mid))
		i += ReadTillEnd(sector, IDs[i:])
	}
	doc.IDsChains = IDs

	shortIDs := make([]int32, header.SectorTotalForShortSAT*header.SectorActualSize>>2)
	shortSATIDs := doc.getChain(uint32(header.FirstSectorIDForShortSAT))
	i = 0
	for _, sid := range shortSATIDs {
		sSAT := doc.getSector(sid)
		i += ReadTillEnd(sSAT, shortIDs)
	}
	doc.ShortIDsChains = shortIDs

	return nil
}

func (doc *CompoundDocument) initDirectory() error {
	header := doc.Header
	dirChain := doc.getChain(uint32(header.FirstSectorIDForDirectory))
	maxDirInSector := int(header.SectorActualSize) / DirEntrySize
	dirEntries := make([]*DirectoryEntry, len(dirChain)*maxDirInSector)
	for i, id := range dirChain {
		dirSector := doc.getSector(id)
		for j := 0; j < maxDirInSector; j++ {
			tmp := dirSector[j*DirEntrySize : (j+1)*DirEntrySize]
			dirEntries[i*maxDirInSector+j] = *(**DirectoryEntry)(unsafe.Pointer(&tmp))
		}
	}
	doc.Directory = make(map[string]*DirectoryEntry)
	for _, dirEntry := range dirEntries {
		if dirEntry.Type == 0 {
			continue
		}
		nm, _ := UnicodeDecoder.Bytes(dirEntry.Name[:dirEntry.NameBytesCount-2])
		doc.Directory[string(nm)] = dirEntry
	}

	return nil
}

func LoadCompoundDocument(path string) *CompoundDocument {
	fp, err := os.Open(path)
	if err != nil {
		log.Fatalf("文件[%s]打开失败，失败原因：%s\n", path, err.Error())
	}
	defer fp.Close()

	stat, err := fp.Stat()
	if err != nil {
		log.Fatalf("文件[%s]状态读取失败，失败原因：%s\n", path, err.Error())
	}
	buf := make([]byte, stat.Size())

	document := new(CompoundDocument)
	_, err = fp.Read(buf)
	if err != nil {
		log.Fatalf("文件[%s]读取失败，失败原因：%s\n", path, err.Error())
	}
	document.buf = buf

	err = document.initHeaderAndSectionBuffer()
	if err != nil {
		log.Fatalf("文件[%s]初始化文件头失败，失败原因：%s\n", path, err.Error())
	}

	err = document.initIDsChains()
	if err != nil {
		log.Fatalf("文件[%s]初始化ID链失败，失败原因：%s\n", path, err.Error())
	}

	err = document.initDirectory()
	if err != nil {
		log.Fatalf("文件[%s]初始化档案信息失败，失败原因：%s\n", path, err.Error())
	}

	return document
}
